package com.sunday.common.http.apache.sync;

import com.sunday.common.core.constants.StringConstants;
import com.sunday.common.http.apache.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.entity.mime.HttpMultipartMode;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.URIBuilder;
import org.springframework.lang.Nullable;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * HttpClientPoolUtils 类是一个工具类，用于发送HTTP请求。
 * 参考网站
 * https://hc.apache.org/httpcomponents-client-5.2.x/index.html
 * https://github.com/apache/httpcomponents-client/blob/master/httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientHttpExchange.java
 * https://www.cnblogs.com/zhc-hnust/p/12006923.html
 * https://blog.csdn.net/lvhdbb/article/details/96766356
 * https://blog.csdn.net/qielanyu_/article/details/115614762
 * <p>
 * commons-httpclient(停更)与httpclient(继续升级中)
 * <p>
 * HTTP连接池请求,支持http和https请求,
 * <p>
 * 基于org.apache.httpcomponents.client5<artifactId>httpclient5</artifactId>
 * HTTP/1.1协议中共定义了八种方法（有时也叫“动作”）来表明Request-URI指定的资源的不同操作方式：
 * . OPTIONS - 返回服务器针对特定资源所支持的HTTP请求方法。
 * 也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。
 * . HEAD    - 向服务器索要与GET请求相一致的响应，只不过响应体将不会被返回。
 * 这一方法可以在不必传输整个响应内容的情况下，就可以获取包含在响应消息头中的元信息。
 * . GET     - 向特定的资源发出请求。
 * 注意：GET方法不应当被用于产生“副作用”的操作中，例如在web app.中。
 * 其中一个原因是GET可能会被网络蜘蛛等随意访问。
 * . POST    - 向指定资源提交数据进行处理请求（例如提交表单或者上传文件）。
 * 数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
 * . PUT     - 向指定资源位置上传其最新内容。
 * . DELETE  - 请求服务器删除Request-URI所标识的资源。
 * . TRACE   - 回显服务器收到的请求，主要用于测试或诊断。
 * . CONNECT - HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
 * <p>
 * PS:特别注意 ，当连接池资源不足时，会产生
 * org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
 * 错误，一定要重点关注,考虑是否提高并发数量
 * <p>
 * application/x-www-form-urlencoded: 表示使用URL编码的方式来编码表单。如果没有将enctype属性设置为任何值，那么这就是默认值。
 * multipart/form-data: 当用户想上传文件这种二进制等文件或者前面的那个方式不能满足时，使用这种类型的表单
 * text/plain: 文本形式，只发送数据而不进行任何编码时使用。
 * https://blog.csdn.net/her_smile/article/details/92794691
 * <p>
 * MultipartEntityBuilder 中文乱码
 * MultipartEntityBuilder.create().setMode(HttpMultipartMode.RFC6532);
 * multipartEntityBuilder.setCharset(Charset.forName(HTTP.UTF_8));
 * 设置请求的编码格式,填坑，遇到乱码时，第一错觉就是设置请求的编码格式，如果设置这个编码，会导致多文件丢失（具体原因不明）
 * multipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
 * 设置浏览器兼容模式 ，填坑，网上好多文章说，设置成BOWSER_COMPATIBLE 就不会乱码了，问题就出现在这里，设置模式时，应该设置成HttpMultipartMode.RFC6532 才没有出现乱码
 * https://www.codeprj.com/blog/8ef45e1.html
 *
 * @author zsy
 * @since 2022/10/15
 */
@Slf4j
public class HttpClientPoolUtils {

    private static final String DEFAULT_ENCODING = StandardCharsets.UTF_8.toString();

    private static final CloseableHttpClient CLOSEABLE_HTTP_CLIENT = HttpClientPoolBuilder.create().httpClientBuilder().build();

    public static HttpResult doGet(String url) throws Exception {
        return doGet(url, null, null, null, null);
    }

    public static HttpResult doGet(String url, @Nullable CloseableHttpClient httpClient) throws Exception {
        return doGet(url, httpClient, null, null, null);
    }

    public static HttpResult doGet(String url, @Nullable Map<String, String> params) throws Exception {
        return doGet(url, null, null, params, null);
    }

    public static HttpResult doGet(String url, @Nullable Map<String, String> headers, @Nullable Map<String, String> params) throws Exception {
        return doGet(url, null, headers, params, null);
    }

    public static HttpResult doGet(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable Map<String, String> params) throws Exception {
        return doGet(url, httpClient, headers, params, null);
    }

    /**
     * GET
     *
     * @param url
     * @param httpClient
     * @param headers
     * @param params
     * @param encoding
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult doGet(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable Map<String, String> params, @Nullable String encoding) throws Exception {
        assert url != null : "url must not be null";
        /**
         * HttpClient提供了URIBuilder实用程序类，以简化请求URI的创建和修改
         */
        // 创建访问的地址
        URIBuilder uriBuilder = new URIBuilder(url);
        if (ObjectUtils.isNotEmpty(params)) {
            params.forEach((k, v) -> uriBuilder.setParameter(k, v));
        }
        // 创建HTTP对象
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        // 设置请求头
        setHeader(httpGet, headers);
        return getHttpClientResult(httpGet, httpClient, encoding);
    }

    /**
     * POST
     *
     * @param url
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult doPost(String url) throws Exception {
        return doPost(url, null, null, (String) null, null);
    }

    public static HttpResult doPost(String url, @Nullable MultipartEntityBuilder multipartEntityBuilder) throws Exception {
        return doPost(url, null, null, multipartEntityBuilder, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable MultipartEntityBuilder multipartEntityBuilder) throws Exception {
        return doPost(url, httpClient, null, multipartEntityBuilder, null);
    }

    public static HttpResult doPost(String url, @Nullable Map<String, String> headers, @Nullable MultipartEntityBuilder multipartEntityBuilder) throws Exception {
        return doPost(url, null, headers, multipartEntityBuilder, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable MultipartEntityBuilder multipartEntityBuilder) throws Exception {
        return doPost(url, httpClient, headers, multipartEntityBuilder, null);
    }

    /**
     * POST MultipartEntityBuilder
     *
     * @param url
     * @param httpClient
     * @param headers
     * @param multipartEntityBuilder
     * @param encoding
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable MultipartEntityBuilder multipartEntityBuilder, @Nullable String encoding) throws Exception {
        // 创建HTTP对象
        HttpPost httpPost = new HttpPost(url);
        // 设置请求头
        setHeader(httpPost, headers);
        // 封装请求参数
        setParam(httpPost, multipartEntityBuilder);
        return getHttpClientResult(httpPost, httpClient, encoding);
    }

    /**
     * POST MultipartEntityBuilder 封装请求参数
     * RFC 822、RFC 2045、RFC 2046兼容
     * STRICT
     * 浏览器兼容模式，即只写Content-Disposition;使用内容字符集
     * EXTENDED
     * RFC 6532 and RFC 7578.
     * <p>
     * 此处一定要用RFC6532，网上普遍用的BROWSER_COMPATIBLE依然会出现中文名乱码
     * multipartEntityBuilder.setCharset(Charset.forName("UTF-8")); //此处踩坑，转发出去的filename依然为乱码
     * ContentType contentType = ContentType.create("multipart/form-data",Charset.forName("UTF-8")); //此处也是坑，转发出去的filename依然为乱码
     *
     * @param httpMethod
     * @param multipartEntityBuilder
     */
    public static void setParam(HttpUriRequestBase httpMethod, @Nullable MultipartEntityBuilder multipartEntityBuilder) {
        if (ObjectUtils.isNotEmpty(multipartEntityBuilder)) {
            multipartEntityBuilder.setMode(HttpMultipartMode.EXTENDED);
            HttpEntity httpEntity = multipartEntityBuilder.build();
            httpMethod.setEntity(httpEntity);
        }
    }

    public static HttpResult doPost(String url, @Nullable String json) throws Exception {
        return doPost(url, null, null, json, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable String json) throws Exception {
        return doPost(url, httpClient, null, json, null);
    }

    public static HttpResult doPost(String url, @Nullable Map<String, String> headers, @Nullable String json) throws Exception {
        return doPost(url, null, headers, json, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable String json) throws Exception {
        return doPost(url, httpClient, headers, json, null);
    }

    /**
     * POST Json
     *
     * @param url
     * @param httpClient
     * @param headers
     * @param json
     * @param encoding
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable String json, @Nullable String encoding) throws Exception {
        // 创建HTTP对象
        HttpPost httpPost = new HttpPost(url);
        // 设置请求头
        setHeader(httpPost, headers);
        // 封装请求参数
        setParam(httpPost, json, encoding);
        return getHttpClientResult(httpPost, httpClient, encoding);
    }

    /**
     * POST Json 封装请求参数
     * HttpClient提供了几个类，可用于通过HTTP连接有效地流式传输内容。这些类的实例可以与诸如POST和的实体封装请求相关联，
     * PUT 以便将实体内容封装到传出的HTTP请求中。HttpClient的提供了几个类为最常见的数据的容器，
     * 如字符串，字节数组，输入流，和文件：StringEntity， ByteArrayEntity， InputStreamEntity，和 FileEntity。
     *
     * @param httpMethod
     * @param json
     * @param encoding
     */
    public static void setParam(HttpUriRequestBase httpMethod, @Nullable String json, @Nullable String encoding) {
        if (StringUtils.isNotBlank(json)) {
            StringEntity stringEntity = new StringEntity(json, ContentType.APPLICATION_JSON, StringUtils.isBlank(encoding) ? DEFAULT_ENCODING : encoding, false);
            /**
             * text/plain: 文本形式，只发送数据而不进行任何编码时使用。
             */
            httpMethod.setEntity(stringEntity);
        }
    }

    public static HttpResult doPost(String url, @Nullable Map<String, String> params) throws Exception {
        return doPost(url, null, null, params, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> params) throws Exception {
        return doPost(url, httpClient, null, params, null);
    }

    public static HttpResult doPost(String url, @Nullable Map<String, String> headers, @Nullable Map<String, String> params) throws Exception {
        return doPost(url, null, headers, params, null);
    }

    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable Map<String, String> params) throws Exception {
        return doPost(url, httpClient, headers, params, null);
    }

    /**
     * POST Map
     *
     * @param url
     * @param httpClient
     * @param headers
     * @param params
     * @param encoding
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult doPost(String url, @Nullable CloseableHttpClient httpClient, @Nullable Map<String, String> headers, @Nullable Map<String, String> params, @Nullable String encoding) throws Exception {
        // 创建HTTP对象
        HttpPost httpPost = new HttpPost(url);
        // 设置请求头
        setHeader(httpPost, headers);
        // 封装请求参数
        setParam(httpPost, params, encoding);
        return getHttpClientResult(httpPost, httpClient, encoding);
    }

    /**
     * POST Map 封装请求参数
     * HTML表格
     * 例如，许多应用程序需要模拟提交HTML表单的过程，以便登录Web应用程序或提交输入数据。
     * HttpClient提供了实体类 UrlEncodedFormEntity来简化该过程。
     *
     * @param httpMethod
     * @param params
     * @param encoding
     */
    public static void setParam(HttpUriRequestBase httpMethod, @Nullable Map<String, String> params, @Nullable String encoding) {
        // 封装请求参数
        if (ObjectUtils.isNotEmpty(params)) {
            List<NameValuePair> nameValuePairs = new ArrayList<>(params.size());
            params.forEach((k, v) -> nameValuePairs.add(new BasicNameValuePair(k, v)));
            /**
             * 设置到请求的http对象中
             * application/x-www-form-urlencoded: 表示使用URL编码的方式来编码表单。如果没有将enctype属性设置为任何值，那么这就是默认值。
             */
            httpMethod.setEntity(new UrlEncodedFormEntity(nameValuePairs, Charset.forName(StringUtils.isBlank(encoding) ? DEFAULT_ENCODING : encoding)));
        }
    }

    /**
     * 获得响应结果
     *
     * @param httpMethod
     * @param httpClient
     * @param encoding
     * @return {@link HttpResult}
     * @throws Exception
     */
    public static HttpResult getHttpClientResult(HttpUriRequestBase httpMethod, @Nullable CloseableHttpClient httpClient, @Nullable String encoding) throws Exception {
        // 本类默认实现连接池,所以不进行数据源的关闭
        CloseableHttpClient closeableHttpClient = httpClient == null ? CLOSEABLE_HTTP_CLIENT : httpClient;
        // 执行请求
        try (CloseableHttpResponse httpResponse = closeableHttpClient.execute(httpMethod)) {
            String result = StringConstants.EMPTY;
            // 获取返回结果
            if (ObjectUtils.isNotEmpty(httpResponse)) {
                if (ObjectUtils.isNotEmpty(httpResponse.getEntity())) {
                    // 获取响应信息
                    result = EntityUtils.toString(httpResponse.getEntity(), StringUtils.isBlank(encoding) ? DEFAULT_ENCODING : encoding);
                    // 确保流被完全消费
                    EntityUtils.consume(httpResponse.getEntity());
                }
                if (HttpStatus.SC_OK != httpResponse.getCode() && StringUtils.isBlank(result)) {
                    result = httpResponse.getReasonPhrase();
                }
                return HttpResult.of(httpResponse.getCode(), result);
            }
            return HttpResult.of(HttpStatus.SC_INTERNAL_SERVER_ERROR, result);
        }
    }

    /**
     * 设置封装请求头
     *
     * @param httpMethod
     * @param headers
     */
    public static void setHeader(HttpUriRequestBase httpMethod, Map<String, String> headers) {
        if (ObjectUtils.isNotEmpty(headers)) {
            // 设置到请求头到HttpRequestBase对象中
            headers.forEach((k, v) -> httpMethod.setHeader(k, v));
        }
    }

}